/*
 * GMoveMakingService.java
 * Created on 12 oct. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.seasoncycle.movemaking;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.user.GComputerUser;
import com.hockeo.vhbase.data.dao.user.GHumanUser;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.error.GNotImplementedException;

/**
 * Service class that is responsible for the move-making process.
 *
 * @version $Id$
 * @author jjanke
 */
@Transactional( propagation = Propagation.MANDATORY )
public class GMoveMakingService
{
  private static Log                               d_logger = LogFactory.getLog( GMoveMakingService.class );
  private IGDao                                    d_dao;
  private GMMFactory                               d_mmFactory;
  private int                                      d_nNumThreads;
  private long                                     d_lTimeoutAllTasksMillis;
  private long                                     d_lTimeoutSingleTaskMillis;

  // Members required for computer move-making control
  //
  private HashMap<Integer, GMoveMakingMatchResult> d_mapMatchResults;
  private HashMap<String, Boolean>                 d_mapIsLastMoveday;
  private Queue<Future<GMMResult>>                 d_queueFutures;
  private ExecutorService                          d_executorService;

  // Stats
  //
  private int                                      d_cMatches;
  private int                                      d_cElements;
  private long                                     d_lMoveMakingTimeCumul;
  private long                                     d_lMoveMakingTimeMin;
  private long                                     d_lMoveMakingTimeMax;

  @Required
  public void setDao( final IGDao dao )
  {
    d_dao = dao;
  }

  @Required
  public void setMoveMakingFactory( final GMMFactory factory )
  {
    d_mmFactory = factory;
  }

  @Required
  public void setNumThreads( final int nNumThreads )
  {
    if ( d_nNumThreads > 0 )
      d_nNumThreads = nNumThreads;
    else
      d_nNumThreads = Runtime.getRuntime().availableProcessors();
  }

  @Required
  public void setTimeoutAllTasks( final long lMillis )
  {
    d_lTimeoutAllTasksMillis = lMillis;
  }

  @Required
  public void setTimeoutSingleTask( final long lMillis )
  {
    d_lTimeoutSingleTaskMillis = lMillis;
  }

  /**
   * Makes the moves for the given moveday. In order to set the moves that have been
   * calculated by this method, the method {@link #syncMatchesWithComputerMoves()} must be
   * called once this method has returned.
   *
   * @param moveday the moveday for which to make the moves
   * @throws GDataException
   */
  @Transactional( readOnly = true, propagation = Propagation.REQUIRES_NEW, rollbackFor = { GException.class } )
  public void makeMoves( final GMoveday moveday ) throws GException
  {
    if ( !moveday.isActive() || !moveday.isPrepared() || moveday.isMovesMade() )
      return;

    // Make computer moves and check human moves
    //
    makeComputerMoves( moveday, d_dao.findAllEntities( GComputerUser.class ) );
    checkHumanMoves( moveday, d_dao.findAllEntities( GHumanUser.class ) );
  }

  /**
   * Synchronizes all matches with the results that have been computed during the
   * preceding call to {@link #makeMoves(GMoveday)}. This method is run in the same
   * transaction as the caller to make sure that all results are properly synchronized.
   *
   * @param moveday the moveday for which the matches should be synchronized
   */
  public void syncMatchesWithComputerMoves( final GMoveday moveday )
  {
    if ( d_mapMatchResults == null )
      throw new IllegalStateException( "There are no results to be synchronized." );

    d_logger.info( "* Setting calculated goals to matches..." );

    for ( GMoveMakingMatchResult mmr : d_mapMatchResults.values() )
      d_dao.placeGoalsToMatch( mmr.getMatchId(), mmr.getPlacedGoalsHome(), mmr.getPlacedGoalsAway() );

    if ( d_logger.isInfoEnabled() )
      d_logger.info( "* Finished processing " + d_mapMatchResults.size() + " match results." );
  }

  /**
   * Makes the moves for all computer controlled users.
   *
   * @param moveday the moveday for which the moves should be made
   * @param listUsers all computer users
   * @throws GDataException
   */
  private void makeComputerMoves( final GMoveday moveday, final List<GComputerUser> listUsers ) throws GException
  {
    if ( listUsers.isEmpty() )
      return;

    d_logger.info( "* Calculating computer moves..." );

    d_executorService = Executors.newFixedThreadPool( d_nNumThreads );
    d_mapMatchResults = new HashMap<Integer, GMoveMakingMatchResult>();
    d_mapIsLastMoveday = new HashMap<String, Boolean>();
    d_queueFutures = new LinkedList<Future<GMMResult>>();

    // Stats
    //
    long lStartTime = System.currentTimeMillis();
    int cSubmittedElements = 0;

    d_cMatches = 0;
    d_cElements = 0;
    d_lMoveMakingTimeCumul = 0;
    d_lMoveMakingTimeMin = -1;
    d_lMoveMakingTimeMax = -1;

    try
    {
      for ( GComputerUser user : listUsers )
      {
        for ( GClub club : user.getCurrentClubs() )
        {
          for ( GContestElement element : d_dao.findContestElements( moveday, club ) )
          {
            cSubmittedElements++;
            executeMoveMaker( moveday, user, club, element );

            if ( d_logger.isInfoEnabled() && cSubmittedElements % 100 == 0 )
              d_logger.info( "** Submitted " + cSubmittedElements + " club/elements for move calculation." );
          }
        }
      }

      if ( d_logger.isInfoEnabled() )
        d_logger.info( "** Finished submitting all elements (" + cSubmittedElements + " in total)." );

      // Wait for all move-making tasks to complete
      //
      d_executorService.shutdown();
      try
      {
        d_executorService.awaitTermination( d_lTimeoutAllTasksMillis, TimeUnit.MILLISECONDS );
      }
      catch ( InterruptedException ex )
      {
        throw new GDataException( "Thread interrupted while waiting for all moves to be made.", ex );
      }

      // Handle all remaining unhandled move-making task results
      //
      while ( !d_queueFutures.isEmpty() )
        handleMoveMakingResult( d_queueFutures.remove() );

      // Display stats
      //
      if ( d_logger.isInfoEnabled() )
      {
        long lDuration = System.currentTimeMillis() - lStartTime;
        double dPerElem = d_cElements > 0 ? lDuration / d_cElements : 0;
        double dPerMatch = d_cMatches > 0 ? lDuration / d_cMatches : 0;
        double dMatchesPerElem = d_cElements > 0 ? d_cMatches / d_cElements : 0;
        double dMMPerElem = d_cElements > 0 ? d_lMoveMakingTimeCumul / d_cElements : 0;
        double dMMPerMatch = d_cMatches > 0 ? d_lMoveMakingTimeCumul / d_cMatches : 0;

        StringBuilder sb = new StringBuilder( "Movemaking statistics (all times in ms):\n" );
        sb.append( "* Number of threads            : " ).append( d_nNumThreads ).append( "\n" );
        sb.append( "* Number of elements w. matches: " ).append( d_cElements ).append( "\n" );
        sb.append( "* Number of empty elements     : " ).append( cSubmittedElements - d_cElements ).append( "\n" );
        sb.append( "* Number of matches            : " ).append( d_cMatches ).append( "\n" );
        sb.append( "* Avg. matches/element         : " ).append( dMatchesPerElem ).append( "\n" );
        sb.append( "* ------------------------------\n" );
        sb.append( "* Total execution time         : " ).append( lDuration ).append( "\n" );
        sb.append( "* Total time/element           : " ).append( dPerElem ).append( "\n" );
        sb.append( "* Total time/match             : " ).append( dPerMatch ).append( "\n" );
        sb.append( "* ------------------------------\n" );
        sb.append( "* Move-making execution time   : " ).append( d_lMoveMakingTimeCumul ).append( "\n" );
        sb.append( "* Move-making time/element     : " ).append( dMMPerElem ).append( "\n" );
        sb.append( "* Move-making time/match       : " ).append( dMMPerMatch ).append( "\n" );
        sb.append( "* Move-making maximum time     : " ).append( d_lMoveMakingTimeMax ).append( "\n" );
        sb.append( "* Move-making minimum time     : " ).append( d_lMoveMakingTimeMin ).append( "\n" );

        d_logger.info( sb.toString() );
      }
    }
    finally
    {
      // Orderly shutdown executor service
      //
      d_executorService.shutdownNow();
      d_executorService = null;
    }
  }

  /**
   * Executes a move-making task. Prepares the move-maker and submits it to the execution
   * service.
   *
   * @param moveday the current moveday
   * @param user the user for which to execute the move(s)
   * @param club the club for which to execute the move(s)
   * @param element the element in which all matches for which moves have to be made are
   *          played
   * @param listMatches the list of matches for which moves should be made
   * @throws GDataException
   */
  private void executeMoveMaker( final GMoveday moveday, final GComputerUser user, final GClub club,
      final GContestElement element ) throws GException
  {
    // Prepare move maker callable
    //
    GMMCallable mmCallable = d_mmFactory.createMoveMakerCallable( user, club, element, moveday, isLastMoveday( moveday,
        element ) );

    // Submit the move maker to the executor
    //
    d_queueFutures.add( d_executorService.submit( mmCallable ) );
  }

  /**
   * Handles a move-making result. Retrieves the calculated number of goals and sets them
   * to the matches for which the moves were made.
   *
   * @param result the move making result to handle
   * @throws GException
   */
  private void handleMoveMakingResult( final Future<GMMResult> future ) throws GException
  {
    GMMResult result;

    try
    {
      result = future.get( d_lTimeoutSingleTaskMillis, TimeUnit.MILLISECONDS );
    }
    catch ( InterruptedException ex )
    {
      throw new GException( "Move making was unexpectedly interrupted", ex );
    }
    catch ( ExecutionException ex )
    {
      throw new GException( "Move making failed because of an exception.", ex );
    }
    catch ( TimeoutException ex )
    {
      throw new GException( "Timeout reached while waiting for move making result.", ex );
    }

    // Place the goals into the match result objects (setting the goals to the matches
    // must be done outside of the transaction to avoid match out of sync issues)
    //
    if ( result.hasMatches() )
    {
      int[] anGoals = result.getGoalsToPlace();
      List<GMatch> listMatches = result.getMatches();
      GClub club = result.getClub();
      for ( int i = 0; i < anGoals.length; i++ )
      {
        GMatch match = listMatches.get( i );
        GMoveMakingMatchResult mmr = d_mapMatchResults.get( match.getId() );

        if ( mmr == null )
        {
          mmr = new GMoveMakingMatchResult( match.getId() );
          d_mapMatchResults.put( match.getId(), mmr );
        }

        mmr.placeGoals( club, match, anGoals[i] );
      }

      d_cMatches += listMatches.size();
      d_cElements++;
    }

    // Stats
    //
    long lExecTime = result.getExecTime();

    d_lMoveMakingTimeCumul += lExecTime;

    if ( d_lMoveMakingTimeMin == -1 || lExecTime < d_lMoveMakingTimeMin )
      d_lMoveMakingTimeMin = lExecTime;

    if ( lExecTime > d_lMoveMakingTimeMax )
      d_lMoveMakingTimeMax = lExecTime;
  }

  /**
   * Checks whether the given moveday is the last for the given contest element.
   *
   * @param moveday the moveday which should be checked
   * @param element the contest element
   * @return <code>true</code> if it is the last moveday, <code>false</code> otherwise
   */
  private boolean isLastMoveday( final GMoveday moveday, final GContestElement element )
  {
    Boolean fIsLast = d_mapIsLastMoveday.get( element.getId() );

    if ( fIsLast == null )
    {
      GMoveday movedayLast = d_dao.findLastMoveday( element );
      fIsLast = Boolean.valueOf( moveday.equals( movedayLast ) );
      d_mapIsLastMoveday.put( element.getId(), fIsLast );
    }

    return fIsLast.booleanValue();
  }

  /**
   * Checks the moves of all human players.
   *
   * @param moveday the moveday for which the moves should be checked
   * @param listUsers all human users
   */
  private void checkHumanMoves( final GMoveday moveday, final List<GHumanUser> listUsers )
  {
    if ( listUsers.isEmpty() )
      return;

    d_logger.info( "* Checking human players' moves..." );

    // TODO check if moves have been made and create NMRs if not
    throw new GNotImplementedException();
  }
}
